# -*- coding: utf-8 -*-

####
#
# 自定义阿里云DNS API实现，无需安装SDK依赖库
#
####
from getpass import getpass
import readline
import sys
import re
import getopt
import os
import configparser
import json
import hashlib
import hmac
import base64
import time
import urllib.parse
from typing import Dict, List
from urllib.request import Request, urlopen
from urllib.error import HTTPError


_SUPPORT_DOMAIN_NAMES = []

_DEFAULT_CONFIG_FILE_PATH = os.path.dirname(os.path.abspath(__file__)) + "/alidns.conf"


class AliyunDNSClient:
    """自定义阿里云DNS API客户端"""

    def __init__(self, access_key_id: str, access_key_secret: str):
        self.access_key_id = access_key_id
        self.access_key_secret = access_key_secret
        self.endpoint = "https://alidns.aliyuncs.com"
        self.version = "2015-01-09"

    def _sign_request(self, params: Dict) -> str:
        """生成签名"""
        # 添加公共参数
        params.update({
            'Format': 'JSON',
            'Version': self.version,
            'AccessKeyId': self.access_key_id,
            'SignatureMethod': 'HMAC-SHA1',
            'Timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
            'SignatureVersion': '1.0',
            'SignatureNonce': str(int(time.time() * 1000))
        })

        # 排序参数
        sorted_params = sorted(params.items())

        # 构建规范化查询字符串
        canonicalized_query_string = '&'.join([f'{urllib.parse.quote(k)}={urllib.parse.quote(str(v))}'
                                               for k, v in sorted_params])

        # 构建签名字符串
        string_to_sign = 'GET&%2F&' + urllib.parse.quote(canonicalized_query_string)

        # 计算签名
        key = self.access_key_secret + '&'
        signature = base64.b64encode(
            hmac.new(key.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha1).digest()
        ).decode('utf-8')

        return signature

    def _make_request(self, action: str, params: Dict) -> Dict:
        """发送API请求"""
        params['Action'] = action
        signature = self._sign_request(params)
        params['Signature'] = signature

        # 构建URL
        query_string = '&'.join([f'{k}={urllib.parse.quote(str(v))}' for k, v in params.items()])
        url = f"{self.endpoint}/?{query_string}"

        request = Request(url)
        response = urlopen(request)
        result = json.loads(response.read().decode('utf-8'))
        return result

    def add_domain_record(self, domain_name: str, rr: str, record_type: str, value: str, ttl: int = 600) -> Dict:
        """添加域名记录"""
        params = {
            'DomainName': domain_name,
            'RR': rr,
            'Type': record_type,
            'Value': value,
            'TTL': ttl
        }
        return self._make_request('AddDomainRecord', params)

    def describe_domain_records(self, domain_name: str, rrkey_word: str = '',
                                value_key_word: str = '', record_type: str = '',
                                page_size: int = 300) -> Dict:
        """查询域名记录"""
        params = {
            'DomainName': domain_name,
            'PageSize': page_size
        }
        if rrkey_word:
            params['RRKeyWord'] = rrkey_word
        if value_key_word:
            params['ValueKeyWord'] = value_key_word
        if record_type:
            params['Type'] = record_type

        return self._make_request('DescribeDomainRecords', params)

    def delete_domain_record(self, record_id: str) -> Dict:
        """删除域名记录"""
        params = {
            'RecordId': record_id
        }
        return self._make_request('DeleteDomainRecord', params)


def _init_client(access_key_id: str = '', access_key_secret: str = '', config_file_path: str = '') -> AliyunDNSClient:
    """
    " Initialize alidns client.
    """

    if access_key_id and access_key_secret:
        return AliyunDNSClient(access_key_id=access_key_id,
                               access_key_secret=access_key_secret)
    conf_path = config_file_path or _DEFAULT_CONFIG_FILE_PATH
    if not os.path.exists(conf_path):
        return None  # NOSONAR

    conf = configparser.ConfigParser()
    conf.read(conf_path)

    if "domains" in conf['DEFAULT']:
        support_domains = conf["DEFAULT"]["domains"].strip()
        if support_domains:
            domains = support_domains.split(",")
            _SUPPORT_DOMAIN_NAMES.clear()
            for domain in domains:
                _SUPPORT_DOMAIN_NAMES.append(domain.strip().strip("'\"").strip())

    return AliyunDNSClient(access_key_id=conf["DEFAULT"]["access_key_id"],
                           access_key_secret=conf["DEFAULT"]["access_key_secret"])


alidns_client: AliyunDNSClient = None  # NOSONAR


def init_alidns_client(access_key_id: str = '', access_key_secret: str = '', config_file_path: str = ''):
    """
    " Initialize alidns client.
    """
    global alidns_client
    alidns_client = _init_client(access_key_id, access_key_secret, config_file_path)


def add_record(rr: str, type: str, value: str, domain_name: str = ''):
    """
    " Add a record to alidns.
    """
    print(f"增加域名 {rr}.{domain_name} 解析类型: {type} 值: {value} ", end="")
    try:
        res = alidns_client.add_domain_record(domain_name=domain_name, rr=rr, record_type=type, value=value)
        rid = res["RecordId"]
        print(f"... 成功! RecordId: {rid}")
        return rid
    except HTTPError as e:
        print(f"失败！ {e.read().decode('utf-8')}")
    except Exception as e:
        print(f'... 失败! {e}')


def add_records(rr: str, type: str, values: List[str], with_wildcard=False, domain_name: str = ''):
    """
    " Add records to alidns.
    """
    _rr = rr or '@'
    for v in values:
        add_record(_rr, type, v, domain_name=domain_name)
        if _rr != '@' and not _rr.startswith('*') and with_wildcard:
            add_record(f"*.{_rr}", type, v, domain_name=domain_name)


def add_a_records(rr: str, ips: List[str], with_wildcard=False, domain_name: str = ''):
    """
    " Add A records to alidns.
    """
    real_ips = []
    for ip in ips:
        ip_parts = ip.split('.')
        if len(ip_parts) != 4:
            print("IP 格式错误!")
            raise Exception("ip error!")  # NOSONAR

        if ip_parts[-1].find('-') >= 0:
            ipls = ip_parts[-1].split('-')
            ipl_s = int(ipls[0])
            ipl_e = int(ipls[1])
        else:
            ipl_s = ipl_e = int(ip_parts[-1])
        for ipp in range(ipl_s, ipl_e + 1):
            aip = '.'.join(ip_parts[0:-1]) + '.' + str(ipp)
            if aip not in real_ips:
                real_ips.append(aip)
    add_records(rr, "A", real_ips, with_wildcard=with_wildcard, domain_name=domain_name)


def add_aaaa_records(rr: str, ips: List[str], with_wildcard=False, domain_name: str = ''):
    """
    " Add AAAA records to alidns.
    """
    real_ips = []
    for ip in ips:
        ip_parts = ip.split(':')
        if ip_parts[-1].find('-') >= 0:
            ipls = ip_parts[-1].split('-')
            ipl_s = int(ipls[0], 16)
            ipl_e = int(ipls[1], 16)
        else:
            ipl_s = ipl_e = int(ip_parts[-1], 16)
        for ipp in range(ipl_s, ipl_e + 1):
            aip = ':'.join(ip_parts[0:-1]) + ':' + hex(ipp)[2:]
            if aip not in real_ips:
                real_ips.append(aip)

    add_records(rr, "AAAA", real_ips, with_wildcard=with_wildcard, domain_name=domain_name)


def add_cname_records(rr: str, cnames: List[str], with_wildcard=False, domain_name: str = ''):
    """
    " Add CNAME records to alidns.
    """
    add_records(rr, "CNAME", cnames, with_wildcard=with_wildcard, domain_name=domain_name)


def add_txt_records(rr: str, txts: List[str], with_wildcard=False, domain_name: str = ''):
    """
    " Add TXT records to alidns.
    """
    add_records(rr, "TXT", txts, with_wildcard=with_wildcard, domain_name=domain_name)


def print_add_help():
    """
    " Print help for add command.
    """
    name_hint = ""
    if _SUPPORT_DOMAIN_NAMES:
        name_hint = f"，支持 {_SUPPORT_DOMAIN_NAMES[0]}"
    if len(_SUPPORT_DOMAIN_NAMES) > 1:
        name_hint += f"(默认) 和 {_SUPPORT_DOMAIN_NAMES[1:]}"
    print(
        f"""

    add -w -r [record] -4 [ipv4 range] -6 [ipv6 range] -n [domain name]

    参数:
    -r    --rr             需要解析的域名片段，默认情况下，会同时增加子域名匹配和该子域名泛域名解析。
    -4    --ipv4           ip 地址段，仅最后一段支持范围，例如: 10.95.100.1 或者 10.95.100.3-10
    -6    --ipv6           ip 地址段，仅最后一段支持范围，例如: abc:ef1::134 或者 abc:ef1::134-14a
    -t    --txt            增加一个 TXT 类型的域名解析。
    -c    --cname          增加一个 CNAME 类型的域名解析。
    -n    --name           网站名称{name_hint}
    -a    --address        完整域名，例如 mpa.ctyun.store
    -w    --wildcard       同时增加泛域名解析。

    例子：
    增加 hn6oss.ctyunxs.cn 和 *.hn6oss.ctyunxs.cn

    add -w -r hn6oss -4 113.219.152.5-8 -6 240e:982:9401:1000::5-8
        """)


def break_address(addr: str) -> tuple[str, str]:
    """
    " Break a full address into rr and domain name.
    """
    arr = addr.split('.')
    rr = '.'.join(arr[0:-2])
    name = '.'.join(arr[-2:])
    return rr, name


def add_records_cmd(argv):  # NOSONAR
    """
    " Add records command line handler.
    """
    try:
        opts = getopt.getopt(argv, "r:4:6:t:c:n:a:hw",
                             ["rr=", "ipv4=", "ipv6=", "txt=", "cname=", "name=", "address=", "help", "wildcard"])[0]
        rrs = []
        ipv4s = []
        ipv6s = []
        txts = []
        cnames = []

        domain_name = ''
        with_wildcard = False

        for opt, val in opts:
            if opt in ('-h', '--help'):
                print_add_help()
                return
            elif opt in ('-r', '--rr'):
                rrs.append(val)
            elif opt in ('-4', '--ipv4'):
                ipv4s.append(val)
            elif opt in ('-6', '--ipv6'):
                ipv6s.append(val)
            elif opt in ('-t', '--txt'):
                txts.append(val)
            elif opt in ('-c', '--cname'):
                cnames.append(val)

            elif opt in ('-w', '--wildcard', ):
                with_wildcard = True
            elif opt in ('-n', '--name'):
                domain_name = val
            elif opt in ('-a', '--address'):
                rr, domain_name = break_address(val)
                rrs.append(rr)

        if domain_name and _SUPPORT_DOMAIN_NAMES and domain_name not in _SUPPORT_DOMAIN_NAMES:
            print(f"[warning] 不支持增加域名 {domain_name}, 请从以下域名中选择： {_SUPPORT_DOMAIN_NAMES[0:]}")
            return

        if not domain_name:
            if not _SUPPORT_DOMAIN_NAMES:
                print("`请通过`-n`或者`--name`参数指定网站名称`！")
                return
            domain_name = _SUPPORT_DOMAIN_NAMES[0]

        if not rrs:
            rrs = ['@']

        if not ipv4s and not ipv6s and not txts and not cnames:
            print_add_help()
            return
        if ipv4s:
            for rr in rrs:
                add_a_records(rr, ipv4s, with_wildcard=with_wildcard, domain_name=domain_name)
        if ipv6s:
            for rr in rrs:
                add_aaaa_records(rr, ipv6s, with_wildcard=with_wildcard, domain_name=domain_name)
        if cnames:
            for rr in rrs:
                add_cname_records(rr, cnames, with_wildcard=with_wildcard, domain_name=domain_name)
        if txts:
            for rr in rrs:
                add_txt_records(rr, txts, with_wildcard=with_wildcard, domain_name=domain_name)
    except getopt.GetoptError as e:
        print(f"{e}")
    except HTTPError as e:
        print(f"{e}:{e.read().decode('utf-8')}")
    except Exception as e:
        print_add_help()


def find_records(rr_keyword: str = '', value_keyword: str = '', record_type: str = '', with_wildcard: bool = True, domain_name: str = '', page_size: int = 300):
    """
    " Find records from alidns.
    """
    res = alidns_client.describe_domain_records(domain_name=domain_name,
                                                rrkey_word=rr_keyword,
                                                value_key_word=value_keyword,
                                                record_type=record_type,
                                                page_size=page_size)
    records: List[Dict[str, str]] = res["DomainRecords"]["Record"]
    for record in records:
        rrr = record['RR']
        if rrr.startswith('*') and not with_wildcard:
            continue
        yield record


def print_recordes(records: list):
    cols = {
        "RowNum": 9,
        "RecordId": 23,
        "RR": 16,
        "DomainName": 15,
        "Type": 8,
        "Status": 10,
        "TTL": 6,
        "Weight": 8,
        "Value": 15
    }

    for record in records:
        rrlen = len(record["RR"])
        if rrlen > cols["RR"]:
            cols["RR"] = rrlen + 2
        idlen = len(record["RecordId"])
        if idlen > cols["RecordId"]:
            cols["RecordId"] = idlen + 2
        dnlen = len(record["DomainName"])
        if dnlen > cols["DomainName"]:
            cols["DomainName"] = dnlen + 2
        tlen = len(record["Type"])
        if tlen > cols["Type"]:
            cols["Type"] = tlen + 2
        valen = len(record["Value"])
        if valen > cols["Value"]:
            cols["Value"] = valen + 2

    row_format = ''
    headers = {}
    seps = {}
    for col_name, col_width in cols.items():
        row_format += f"{{{col_name}:<{col_width}}}"
        headers[col_name] = col_name
        seps[col_name] = '-' * col_width

    print()
    print(row_format.format_map(headers))
    print(row_format.format_map(seps))
    for idx in range(len(records)):
        rd = RowDict(records[idx])
        rd['RowNum'] = idx + 1
        print(row_format.format_map(rd))
    print()


def print_find_help(for_delete: bool = False):
    """
    " Print help for find command.
    """
    cmd = 'delete' if for_delete else 'find'
    name_hint = ""
    if _SUPPORT_DOMAIN_NAMES:
        name_hint = f"，支持 {_SUPPORT_DOMAIN_NAMES[0]}"
    if len(_SUPPORT_DOMAIN_NAMES) > 1:
        name_hint += f"(默认) 和 {_SUPPORT_DOMAIN_NAMES[1:]}"

    print(f"""
    {cmd} -r [record keyword] -v [value keyword] -n [domain name]

    参数:
    -r    --rr             需要查询的域名片段关键字，模糊查询。
    -v    --value          需要查询的 Value 值关键字，可以在此传入 IP 来取得对应 IP。
    -t    --type           需要查询的类型，分别为 A/AAAA/TXT/CNAME。
    -a    --address        需要查询的完整地址。
    -n    --name           网站名称{name_hint}
          --no-wildcard    结果中不包含泛域名解析
    """)


class RowDict(dict):
    """
    " A dict that returns '-' when key is missing.
    """

    def __missing__(self, key):
        """
        " When key is missing, return '-'.
        """
        return "-"


def find_records_cmd(argv, for_delete: bool = False):  # NOSONAR
    """
    " Find records command line handler.
    """
    try:
        opts = getopt.getopt(argv, "r:v:n:t:ha:",
                             ["rr=", 'value=', "type=", "no-wildcard", "name=", "help", "address="])[0]
        rr = ''
        v = ''
        rtype = ''
        domain_name = ''
        nw = False
        for opt, val in opts:
            if opt in ('-h', '--help'):
                print_find_help(for_delete=for_delete)
                return
            if opt in ('-r', '--rr'):
                rr = val
            elif opt in ('-v', '--value'):
                v = val
            elif opt in ('-t', '--type', ):
                rtype = val
            elif opt in ('--no-wildcard', ):
                nw = True
            elif opt in ('-n', '--name'):
                domain_name = val
            elif opt in ('-a', '--address'):
                rr, domain_name = break_address(val)

        if domain_name and _SUPPORT_DOMAIN_NAMES and domain_name not in _SUPPORT_DOMAIN_NAMES:
            print(f"[warning] 不支持 {domain_name}，请从以下域名中选择： {_SUPPORT_DOMAIN_NAMES[0:]}")
            return

        if not domain_name:
            if not _SUPPORT_DOMAIN_NAMES:
                print("请通过`-n`或者`--name`参数指定网站名称！")
                return
            domain_name = _SUPPORT_DOMAIN_NAMES[0]

        if not rr and not v:
            print_find_help(for_delete=for_delete)
            return

        rs = list(find_records(rr_keyword=rr,
                               value_keyword=v,
                               record_type=rtype,
                               with_wildcard=not nw,
                               domain_name=domain_name))
        print_recordes(rs)
        return rs
    except getopt.GetoptError as e:
        print(f"{e}")
    except HTTPError as e:
        print(f"{e}:{e.read().decode('utf-8')}")
    except Exception as e:
        print(f"{e}")
        print_find_help(for_delete=for_delete)


def delete_record(record_id: str):
    """
    " Delete a record from alidns.
    """
    print(f"删除记录 [#{record_id}] ... ", end="")
    try:
        alidns_client.delete_domain_record(record_id=record_id)
        print("成功！")
    except HTTPError as e:
        print(f"失败！ {e.read().decode('utf-8')}")
    except Exception as e:
        print(f"失败！ {e}")


def delete_records_cmd(argvs):  # NOSONAR
    """
    " Delete records command line handler.
    """
    rs = find_records_cmd(argvs, True)

    if not rs:
        return

    row_nums = input(
        "请输入需要删除的行号，删除多行以`,`分割开。A删除所有行，N不执行删除。 [RowNum](,[RowNum]...)/A(ll)/N(one): ")
    readline.remove_history_item(readline.get_current_history_length() - 1)
    print()

    if row_nums.upper() in ("N", "NONE"):
        return
    elif row_nums.upper() in ("A", "ALL"):
        for r in rs:
            delete_record(r["RecordId"])
    else:
        rns = row_nums.split(',')
        idxs = []
        for r in rns:
            try:
                idx = int(r.strip())
                if idx < 0 or idx > len(rs):
                    print(f"行号 {idx} 超出范围。")
                    return
                idxs.append(idx)
            except Exception:
                print(
                    f"请输入正确的行号，而不是：{r}")
                return

        for idx in idxs:
            delete_record(rs[idx-1]['RecordId'])


def break_into_args(line):
    """
    " Break a shell command line into args list.
    """
    dquotes: List[str] = re.findall(r'\s"[^"]+"', line)
    for idx in range(len(dquotes)):
        dqv = dquotes[idx]
        line = line.replace(dqv, f" $$$$shell_line_dquotes{idx}")

    quotes: List[str] = re.findall(r"\s'[^']+'", line)
    for idx in range(len(quotes)):
        dqv = quotes[idx]
        line = line.replace(dqv, f" $$$$shell_line_quotes{idx}")

    shell_args: List[str] = re.split(r"\s+", line)

    args = []
    for p in shell_args:
        p = p.strip()
        if p.startswith('$$$$shell_line_dquotes'):
            idx = p[len('$$$$shell_line_dquotes')]
            idx = int(idx)
            args.append(dquotes[idx].strip().strip('"'))
        elif p.startswith('$$$$shell_line_quotes'):
            idx = p[len('$$$$shell_line_quotes')]
            idx = int(idx)
            args.append(quotes[idx].strip().strip("'"))
        else:
            args.append(p)

    return args


def print_history():
    """
    " Print command history.
    """
    print()
    for idx in range(0, readline.get_current_history_length()):
        print(readline.get_history_item(idx + 1))
    print()


def init_alindns_client_via_input():
    """
    " Initialize alidns client via input.
    """
    try:
        print("Alindns Client 尚未初始化，请输入 access_key_id 和 access_key_secret 或者配置文件路径来初始化。")
        akid = getpass("access_key_id: ").strip()
        aks = getpass("access_key_secret: ").strip()
        if not akid or not aks:
            conf_path = input("配置文件路径: ").strip()
            readline.remove_history_item(readline.get_current_history_length() - 1)
            init_alidns_client(config_file_path=conf_path)
        else:
            init_alidns_client(access_key_id=akid, access_key_secret=aks)
    except Exception:
        pass


def do_cmd(opt_type, args, msg: str = "请输入操作类型 add/find/delete"):
    """
    " Do a command.
    """
    if alidns_client is None:
        init_alindns_client_via_input()
    if alidns_client is None:
        print("Alindns Client 初始化失败，请检查 access_key_id 和 access_key_secret 或者配置文件路径是否正确。")
        return
    if opt_type == 'add':
        add_records_cmd(args)
    elif opt_type == 'find':
        find_records_cmd(args)
    elif opt_type in ('delete', "del"):
        delete_records_cmd(args)
    elif opt_type == 'history':
        print_history()
    else:
        print(msg)


def shell():
    """
    " A simple shell to interact with alidns.
    """
    if alidns_client is None:
        init_alindns_client_via_input()
    if alidns_client is None:
        print("Alindns Client 初始化失败，请检查 access_key_id 和 access_key_secret 或者配置文件路径是否正确。")
        return
    msg = "输入`quit`退出，输入 add/find/delte 来对记录进行操作。"
    print(msg)
    try:
        while True:
            line = input("> ")
            line = line.strip()
            if not line:
                continue
            if line.lower() in ('quit', 'exit', 'q'):
                break
            args = break_into_args(line)
            do_cmd(args[0], args[1:], msg=msg)
    except Exception:
        pass


def main():
    """
    " Main entry point.
    """
    init_alidns_client()
    if len(sys.argv) < 2:
        shell()
        return

    opt_type = sys.argv[1]
    if opt_type == "shell":
        shell()
    else:
        do_cmd(opt_type, sys.argv[2:])


if __name__ == "__main__":
    main()
